﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PServiceBus.Services.Gateway.Runtime;
using System.Collections.Concurrent;
using PServiceBus.Core.Runtime;
using PServiceBus.Core.Runtime.Attributes;
using PServiceBus.Core.Interface;
using PServiceBus.Transports;
using PServiceBus.MessageProxy;
using System.Collections;
using PServiceBus.Core.Runtime.Transports;

namespace PServiceBus.Client {
    public static class PSBClient {

        const string USERNAME_KEY = "pservicebus_username_info";

        static int _port = 8000;
        static bool _inuse = false;

        static string _host = "locahost",
            _apiKey = "demo", _passCode = "demo",
            _username;
        static readonly Dictionary<string, string> _topics =
            new Dictionary<string, string>();
        readonly static Type _topicHeaderAttributeType =
            typeof(TopicHeaderAttribute);
        readonly static ConcurrentDictionary<string, dynamic> _handlers =
            new ConcurrentDictionary<string, dynamic>();

        static PSBClient() {
            TransportType = TransportType.Tcp;
            Address = "localhost:1000:true";
            ESB.OnDisconnect = Disconnect;
        }

        private static void SetState() {
            _inuse = true;
        }

        private static void UnRegisterTopic(string name) {
            if (name == null) throw new ArgumentNullException("name");
            SetState();
            if (Topic.Exists(name)) 
                MethodHelper.Try(() => 
                    Topic.Select(name).Delete());
            if (_topics.ContainsKey(name))
                _topics.Remove(name);
        }

        public static void Register(string name, string description = null, Dictionary<string, string> contract = null) {
            if (name == null) throw new ArgumentNullException("name");
            SetState();
            if (_topics.ContainsKey(name) || Topic.Exists(name)) return;
            var topic = Topic.New(name).Description(description ?? name);
            contract = contract ?? new Dictionary<string, string>();
            foreach (var kv in contract)
                topic.AddParameter(kv.Key, kv.Value);
            topic.Register();
            _topics[name] = name;
        }

        public static void Register<T>() where T : class {
            SetState();
            if (Topic.Exists<T>()) return;
            Topic.Register<T>();
        }

        public static void UnRegister(string name) {
            UnRegisterTopic(name);
        }

        public static void UnRegister<T>() where T : class {
            UnRegisterTopic(typeof(T).Name);
        }

        public static bool Ping() {
            SetState();
            return ESB.IsConnected;
        }

        public static void Update<T>(string filter = null, bool caseSensitive = true) where T : class {
            SetState();
            var type = typeof(T);
            var topicName = type.Name;
            var headerProperty = type.GetProperties().FirstOrDefault(x => x.GetCustomAttributes(_topicHeaderAttributeType, false).Any());
            var needHeader = headerProperty != null;
            filter = filter ?? string.Empty;
            //Auto Register topic if not exists
            Register(topicName);
            Subscriber.Select(UserName)
                .Update(Topic.Select(topicName)
                    .Filter(filter).CaseSensitiveFilter(caseSensitive)
                    .NeedHeaders(needHeader))
                .Save();
        }

        public static void Subscribe<T>(Action<T> callback, string filter = null, TimeSpan? interval = null, int batchSize = 1, bool caseSensitive = true) where T : class {
            if (callback == null) throw new ArgumentNullException("callback");
            SetState();
            filter = filter ?? string.Empty;
            var type = typeof(T);
            var topicName = type.Name;
            var headerProperty = type.GetProperties().FirstOrDefault(x => x.GetCustomAttributes(_topicHeaderAttributeType, false).Any());
            var needHeader = headerProperty != null;
            //Auto Register topic if not exists
            Register(topicName);
            var subscriber = Subscriber.SelectOrAdd(UserName);
            if (!subscriber.Topics.ContainsKey(topicName)) {
                subscriber
                    .SubscribeTo(Topic.Select(topicName)
                        .Filter(filter).CaseSensitiveFilter(caseSensitive)
                        .NeedHeaders(needHeader))
                    .AddTransportFor<T>(topicName, GetTransport(topicName))
                    .Save();
            }
            _handlers[topicName] = subscriber.OnMessageReceived<T>(topicName,
                callback, error => { error.Continue = true; },
                interval: interval ?? TimeSpan.FromMilliseconds(5),
                batchSize: batchSize);
        }

        public static void UnSubscribe<T>() where T : class {
            UnSubscribe(typeof(T).Name);
        }

        public static void UnSubscribe(string topicName) {
            if (topicName == null) throw new ArgumentNullException("topicName");
            SetState();
            var subscriber = Subscriber.Select(UserName);
            if (!subscriber.IsValid) return;
            subscriber.UnSubscribeFrom(topicName);
            dynamic handler;
            if (_handlers.TryRemove(topicName, out handler))
                handler.Stop();
        }

        public static void Publish(string topicName, object message, string groupID = null, int sequenceID = 0, TimeSpan? expires = null, Dictionary<string, string> headers = null) {
            if (topicName == null) throw new ArgumentNullException("topicName");
            SetState();
            var type = message.GetType();
            headers = headers ?? new Dictionary<string, string>();
            if (!String.IsNullOrWhiteSpace(groupID) && sequenceID > 0) {
                headers["ESB_GROUP_ID"] = groupID;
                headers["ESB_SEQUENCE_ID"] = sequenceID.ToString();
            }
            if (!type.IsArray && !typeof(IList).IsAssignableFrom(type))
                message = new[] { message };

            Register(topicName);

            var topic = Topic.Select(topicName)
                .SetMessageExpiration(expires ?? TimeSpan.FromDays(30));
            
            foreach(var kv in headers) topic.SetHeader(kv.Key, kv.Value);
            topic.PublishMany(message as object[]);
        }

        public static void Publish<T>(T message, string groupID = null, int sequenceID = 0, TimeSpan? expiresIn = null, Dictionary<string, string> headers = null) where T : class {
            var type = typeof(T);
            Publish(type.Name, message, groupID, sequenceID, expiresIn, headers);
        }

        private static void Disconnect() {
            var username = UserName;
            if (OnDisconnect != null) OnDisconnect();
            if (!Durable) JsonSettings.Default.RemoveAndSave(USERNAME_KEY);
            TransportHandlers.ShutDown();
            _handlers.Clear();
            if (!Durable) {
                var subscriber = Subscriber.Select(username);
                if (subscriber != null && subscriber.IsValid) subscriber.Delete();
            }
        }

        public static void ReThrowException(bool value) {
            ESB.ReThrowException(value);
        }

        private static void Validate() {
            if (_inuse)
                throw new InvalidOperationException("ESB Configuration cannot be changed once in use");
        }

        public static int Port {
            set {
                Validate();
                ESB.Connect(_host, _port = value);
            }
        }
        public static string Host {
            set {
                Validate();
                ESB.Connect(_host = value, _port);
            }
        }
        public static string ApiKey {
            set {
                Validate();
                ESB.Authenticate(_apiKey = value, _passCode);
            }
        }
        public static string PassCode { 
            set {
                Validate();
                ESB.Authenticate(_apiKey, _passCode = value);
            }
        }

        public static string UserName {
            get {
                if (!String.IsNullOrWhiteSpace(_username)) return _username;
                _username = JsonSettings.Default.Get<string>(USERNAME_KEY);
                if (String.IsNullOrWhiteSpace(_username))
                    _username = String.Concat("DotNet", Guid.NewGuid().ToString());
                if (Durable)
                    JsonSettings.Default.AddAndSave(USERNAME_KEY, _username);
                return _username;
            }
        }

        private static string ParseAddress(string topicName) {
            return String.Concat(Address, topicName, UserName);
        }

        private static ITransport GetTransport(string topicName) {
            switch (TransportType) {
                case TransportType.MSMQ:
                    return Transport.New<MSMQTransport>(t => t.Path = ParseAddress(topicName));
                case TransportType.RabbitMQ:
                    return Transport.New<RabbitMQTransport>(t => t.Path = ParseAddress(topicName));
                case TransportType.Redis:
                    return Transport.New<RedisTransport>(t => t.Path = ParseAddress(topicName));
                default:
                    //Ignore address and only use SSL when needed
                    //Need to find better way to accept TCP address with little modifications
                    var tokens = Address.Split(':');
                    var useSSL = tokens.Length > 2 ?
                        tokens[2].Equals("true", StringComparison.OrdinalIgnoreCase) : false;
                    var transport = TcpHelper.Transport as TcpTransport;
                    transport.UseSSL = useSSL;
                    return transport;
            }
        }

        public static bool Durable { internal get; set; }

        public static string Address { internal get; set; }
        public static TransportType TransportType { internal get; set; }

        public static Action OnDisconnect { get; set; }
    }
}
